package dk.mirasola.systemtraining.biddingsession.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import dk.mirasola.systemtraining.biddingsession.shared.service.SessionNotFoundException;
import dk.mirasola.systemtraining.biddingsession.shared.service.SessionService;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.BidBoardTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.BidInitDataTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.FinishBoardTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.FinishInitDataTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.InitDataTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.MakeBidResultTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.SessionBoardTO;
import dk.mirasola.systemtraining.biddingsession.shared.transfer.StartSessionTO;
import dk.mirasola.systemtraining.bridgewidgets.shared.model.Bid;
import dk.mirasola.systemtraining.bridgewidgets.shared.model.BiddingSequence;
import dk.mirasola.systemtraining.bridgewidgets.shared.model.Hand;
import dk.mirasola.systemtraining.bridgewidgets.shared.model.Seat;
import dk.mirasola.systemtraining.bridgewidgets.shared.model.SpecialBid;
import dk.mirasola.systemtraining.server.MailUtil;
import dk.mirasola.systemtraining.server.ObjectifyUtil;
import dk.mirasola.systemtraining.server.entity.AutoBid;
import dk.mirasola.systemtraining.server.entity.BiddingSession;
import dk.mirasola.systemtraining.server.entity.BiddingSet;
import dk.mirasola.systemtraining.server.entity.Board;
import dk.mirasola.systemtraining.server.i18n.MailTexts;
import dk.mirasola.systemtraining.server.i18n.TextsFactory;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class SessionServiceImpl extends RemoteServiceServlet implements SessionService {
    public StartSessionTO startSession(final String sessionKey) throws SessionNotFoundException {
        Objectify ofy = ObjectifyService.begin();
        Key<BiddingSession> key = null;
        try {
            key = ofy.getFactory().stringToKey(sessionKey);
        } catch (RuntimeException e) {
            throw new SessionNotFoundException();
        }

        BiddingSession biddingSession = ofy.find(key);
        if (biddingSession == null) {
            throw new SessionNotFoundException();
        }

        if (biddingSession.getStarted() == null) {
            biddingSession.setStarted(new Date());
            ofy.put(biddingSession);
        }

        StartSessionTO res = new StartSessionTO();
        res.setBiddingSetKey(ofy.getFactory().keyToString(biddingSession.getBiddingSetKey()));
        res.setActive(biddingSession.getEnded() == null);

        BiddingSet biddingSet = ofy.get(biddingSession.getBiddingSetKey());
        res.setBiddingSetCompleted(biddingSet.getState() == BiddingSet.State.COMPLETED);

        return res;
    }

    public BidInitDataTO getBidInitData(final String biddingSetKeyStr) {
        Objectify ofy = ObjectifyService.begin();
        Key<BiddingSet> biddingSetKey = ofy.getFactory().<BiddingSet>stringToKey(biddingSetKeyStr);
        BiddingSet biddingSet = ofy.get(biddingSetKey);
        List<Board> boards = getBoards(biddingSetKey, ofy);
        BidInitDataTO res = new BidInitDataTO();
        Board firstBoard = populateInitData(res, biddingSet, boards, ofy);
        res.setSeat(biddingSet.getSeatInTurn());
        res.setFirstBoard(createBidBoard(firstBoard, ofy));
        return res;
    }

    public FinishInitDataTO getFinishInitData(final String biddingSetKeyStr) {
        Objectify ofy = ObjectifyService.begin();
        Key<BiddingSet> biddingSetKey = ofy.getFactory().<BiddingSet>stringToKey(biddingSetKeyStr);
        BiddingSet biddingSet = ofy.get(biddingSetKey);
        List<Board> boards = getBoards(biddingSetKey, ofy);
        FinishInitDataTO res = new FinishInitDataTO();
        Board board = populateInitData(res, biddingSet, boards, ofy);
        res.setFirstBoard(createFinishBoard(board, ofy));
        return res;
    }

    public BidBoardTO getBidBoard(final String boardKey) {
        Objectify ofy = ObjectifyService.begin();
        Board board = ofy.get(ofy.getFactory().<Board>stringToKey(boardKey));
        return createBidBoard(board, ofy);
    }

    public FinishBoardTO getFinishBoard(final String boardKey) {
        Objectify ofy = ObjectifyService.begin();
        Board board = ofy.get(ofy.getFactory().<Board>stringToKey(boardKey));
        return createFinishBoard(board, ofy);
    }

    public void addComment(final String boardKey, final String comment) {
        Objectify ofy = ObjectifyService.begin();
        Board board = ofy.get(ofy.getFactory().<Board>stringToKey(boardKey));
        if (board.getComments() == null) {
            board.setComments(new ArrayList<String>());
        }
        board.getComments().add(comment);
        ofy.put(board);
    }

    public BidBoardTO undoBid(final String boardKeyStr) {
        Objectify ofy = ObjectifyService.begin();
        Board board = ofy.get(ofy.getFactory().<Board>stringToKey(boardKeyStr));
        board.getBiddingSequence().undoBid();
        board.getBiddingSequence().undoBid();
        ofy.put(board);
        return createBidBoard(board, ofy);
    }

    public MakeBidResultTO makeBid(final String boardKeyStr, final Bid bid) {
        return ObjectifyUtil.doTransactional(new ObjectifyUtil.ObjectifyCallback<MakeBidResultTO>() {
            @Override
            public MakeBidResultTO doInTransaction(Objectify ofy) {
                Key<Board> boardKey = ofy.getFactory().stringToKey(boardKeyStr);
                Key<BiddingSet> biddingSetKey = boardKey.getParent();
                BiddingSet biddingSet = ofy.get(biddingSetKey);
                List<Board> boards = getBoards(biddingSetKey, ofy);
                List<AutoBid> autoBids = getAutoBids(biddingSetKey, ofy);

                MakeBidResultTO res = new MakeBidResultTO();
                for (Board board : boards) {
                    if (board.getBoardNumber() == boardKey.getId()) {
                        board.getBiddingSequence().makeBid(bid);
                        autoBidForEastOrWest(autoBids, board);
                        res.setBiddingSequenceFinish(board.getBiddingSequence().isFinish());
                        ofy.put(board);
                        break;
                    }
                }

                // find next board to bid
                Board boardToBid = null;
                for (Board b : boards) {
                    if (b.getBiddingSequence().isFinish()) {
                        continue;
                    }
                    if (b.getBiddingSequence().getSeatInTurn().equals(biddingSet.getSeatInTurn())) {
                        boardToBid = b;
                        break;
                    }
                }

                if (boardToBid != null) {
                    res.setNextBoard(createBidBoard(boardToBid, ofy));
                } else {
                    // no more boards to bid
                    // find the active one and close it
                    BiddingSession activeSession = ofy.query(BiddingSession.class)
                            .ancestor(biddingSetKey)
                            .filter("ended", null)
                            .get();
                    activeSession.setEnded(new Date());

                    // create a new active session
                    BiddingSession newBiddingSession = new BiddingSession(biddingSetKey);
                    ofy.put(activeSession, newBiddingSession);

                    if (allBoardsCompleted(boards)) {
                        // all boards is finish so update bidding set state to completed and
                        // send notification mail to both north and south
                        biddingSet.setSeatInTurn(null);
                        biddingSet.setState(BiddingSet.State.COMPLETED);

                        // north
                        MailTexts northTexts = TextsFactory.getTexts(MailTexts.class, biddingSet.getNorthLocale());
                        String northSubject = northTexts.getCompletedNotificationSubject();
                        String northBody = northTexts.getCompletedNotificationBody(ObjectifyUtil.entityKeyToString(newBiddingSession, ofy), biddingSet.getNorthLocale());
                        MailUtil.sendNotificationMail(biddingSet.getNorthEmail().getEmail(), northSubject, northBody);

                        // south
                        MailTexts southTexts = TextsFactory.getTexts(MailTexts.class, biddingSet.getSouthLocale());
                        String southSubject = southTexts.getCompletedNotificationSubject();
                        String southBody = southTexts.getCompletedNotificationBody(ObjectifyUtil.entityKeyToString(newBiddingSession, ofy), biddingSet.getNorthLocale());
                        MailUtil.sendNotificationMail(biddingSet.getSouthEmail().getEmail(), southSubject, southBody);
                    } else {
                        // still unfinished boards so update seat in turn and send notification mail
                        biddingSet.setSeatInTurn(biddingSet.getSeatInTurn().getPartner());
                        autoBidForNorthOrSouthAndSendInTurnNotification(biddingSet, autoBids, boards, newBiddingSession, ofy);
                    }
                    ofy.put(biddingSet);
                }
                return res;
            }
        });
    }


    private static void sendInTurnNotification(BiddingSet biddingSet, BiddingSession newBiddingSession, Objectify ofy) {
        String seatInTurnLocale;
        String email;
        if (biddingSet.getSeatInTurn() == Seat.NORTH) {
            seatInTurnLocale = biddingSet.getNorthLocale();
            email = biddingSet.getNorthEmail().getEmail();
        } else {
            seatInTurnLocale = biddingSet.getSouthLocale();
            email = biddingSet.getSouthEmail().getEmail();
        }

        MailTexts texts = TextsFactory.getTexts(MailTexts.class, seatInTurnLocale);
        String subject = texts.getInTurnNotificationSubject();
        String body = texts.getInTurnNotificationBody(ObjectifyUtil.entityKeyToString(newBiddingSession, ofy), biddingSet.getNorthLocale());
        MailUtil.sendNotificationMail(email, subject, body);
    }

    private boolean allBoardsCompleted(List<Board> boards) {
        for (Board b : boards) {
            if (!b.getBiddingSequence().isFinish()) {
                return false;
            }
        }
        return true;
    }

    public static void autoBidForNorthOrSouthAndSendInTurnNotification(BiddingSet biddingSet, List<AutoBid> autoBids, List<Board> boards, BiddingSession newBiddingSession, Objectify ofy) {
        if (autoBids.size() == 0) {
            sendInTurnNotification(biddingSet, newBiddingSession, ofy);
            return;
        }

        // first find the seat and round we might autobid for
        Seat seatInTurn = null;
        int roundInTurn = -1;
        for (Board board : boards) {
            if (!board.getBiddingSequence().isFinish()) {
                seatInTurn = board.getBiddingSequence().getSeatInTurn();
                roundInTurn = board.getBiddingSequence().getRoundInTurn();
                break;
            }
        }

        // now check if we have an autobid
        AutoBid autoBid = null;
        for (AutoBid ab : autoBids) {
            if (ab.getSeat().equals(seatInTurn) && ab.getRound() == roundInTurn) {
                autoBid = ab;
            }
        }

        if (autoBid != null) {
            boolean autoBidLegalForBoards = true;
            for (Board board : boards) {
                if (board.getBiddingSequence().isBidLegal(autoBid.getBid())) {
                    board.getBiddingSequence().makeBid(autoBid.getBid());
                    autoBidForEastOrWest(autoBids, board);
                    ofy.put(board);
                } else {
                    autoBidLegalForBoards = false;
                }
            }

            if (autoBidLegalForBoards) {
                biddingSet.setSeatInTurn(biddingSet.getSeatInTurn().getPartner());
                autoBidForNorthOrSouthAndSendInTurnNotification(biddingSet, autoBids, boards, newBiddingSession, ofy);
            } else {
                sendInTurnNotification(biddingSet, newBiddingSession, ofy);
            }
        } else {
            sendInTurnNotification(biddingSet, newBiddingSession, ofy);
        }
    }

    private static void autoBidForEastOrWest(List<AutoBid> autoBids, Board board) {
        if (!board.getBiddingSequence().isFinish()) {
            // auto bid for east or west
            boolean madeAutoBid = false;
            for (AutoBid autoBid : autoBids) {
                if (board.getBiddingSequence().getSeatInTurn().equals(autoBid.getSeat())
                        && board.getBiddingSequence().getRoundInTurn() == autoBid.getRound()
                        && board.getBiddingSequence().isBidLegal(autoBid.getBid())) {
                    board.getBiddingSequence().makeBid(autoBid.getBid());
                    madeAutoBid = true;
                    break;
                }
            }
            if (!madeAutoBid) {
                board.getBiddingSequence().makeBid(SpecialBid.PAS);
            }
        }
    }

    private Board populateInitData(InitDataTO res, BiddingSet biddingSet, List<Board> boards, Objectify ofy) {
        res.setBiddingSetName(biddingSet.getName());

        Board firstBoard = null;
        List<SessionBoardTO> sessionBoards = new ArrayList<SessionBoardTO>();
        for (Board board : boards) {
            if (firstBoard == null && biddingSet.getState() == BiddingSet.State.COMPLETED) {
                firstBoard = board;
            }
            SessionBoardTO sessionBoard = new SessionBoardTO();
            sessionBoard.setBoardKey(ofy.getFactory().keyToString(ofy.getFactory().getKey(board)));
            sessionBoard.setBoardNumber((int) board.getBoardNumber());
            if (board.getBiddingSequence().isFinish()) {
                sessionBoard.setState(0);
            } else if (board.getBiddingSequence().getSeatInTurn() == biddingSet.getSeatInTurn()) {
                sessionBoard.setState(1); // to bid
                if (firstBoard == null) {
                    firstBoard = board;
                }
            } else {
                sessionBoard.setState(2); // already bid in session, but not finish
            }
            sessionBoards.add(sessionBoard);
        }
        res.setSessionBoards(sessionBoards);
        return firstBoard;
    }

    private List<Board> getBoards(Key<BiddingSet> biddingSetKey, Objectify ofy) {
        return ofy.query(Board.class)
                .ancestor(biddingSetKey)
                .order("boardNumber")
                .list();
    }

    private List<AutoBid> getAutoBids(Key<BiddingSet> biddingSetKey, Objectify ofy) {
        return ofy.query(AutoBid.class)
                .ancestor(biddingSetKey)
                .list();
    }

    private FinishBoardTO createFinishBoard(Board board, Objectify ofy) {
        FinishBoardTO finishBoard = new FinishBoardTO();
        finishBoard.setBoardKey(ofy.getFactory().keyToString(ofy.getFactory().<Object>getKey(board)));
        finishBoard.setBiddingSequence(board.getBiddingSequence());
        finishBoard.setDistribution(board.getDistribution());
        finishBoard.setComments(board.getComments());
        return finishBoard;
    }

    private BidBoardTO createBidBoard(Board board, Objectify ofy) {
        BiddingSet biddingSet = ofy.get(board.getBiddingSet());
        BidBoardTO bidBoard = new BidBoardTO();
        bidBoard.setBoardKey(ofy.getFactory().keyToString(ofy.getFactory().<Object>getKey(board)));
        bidBoard.setComments(board.getComments());
        bidBoard.setBiddingSequence(board.getBiddingSequence());
        Hand hand = board.getDistribution().getHand(biddingSet.getSeatInTurn());
        bidBoard.setHand(hand);

        BiddingSequence.BackwardIterator i = board.getBiddingSequence().getBackwardIterator();
        boolean undoable = false;
        while (i.hasPrevBid()) {
            i.prevBid();
            String bidderEmail = getBidderEmail(biddingSet, i.getSeat());
            if (bidderEmail != null) {
                undoable = biddingSet.getSeatInTurn().equals(i.getSeat());
                break;
            }
        }
        bidBoard.setLastBidUndoable(undoable);
        return bidBoard;
    }

    private String getBidderEmail(BiddingSet biddingSet, Seat seat) {
        switch (seat) {
            case NORTH:
                return biddingSet.getNorthEmail().getEmail();
            case SOUTH:
                return biddingSet.getSouthEmail().getEmail();
            default:
                return null;
        }
    }
}
